'''
An RPG Modification for Source games using Source Python.

    Copyright (C) 2012 Steven Hartin

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Created on 22 Dec 2012

@author: Steven Hartin
@file: ../sourcerpg/database/cache.py
'''

import copy

from sourcerpg.utils.baseclass import BaseClass


class CacheProperty(object):
    """An object to store a cached property value which holds a dirty flag"""
    def __init__(self, value=None):
        self.__value = value
        self.__dirty = False

    @property
    def value(self):
        """@return mixed The value that this cached property holds"""
        return self.__value

    @value.setter
    def value(self, value):
        """Reassigns this cached property to hold a new value.

        This will update the dirty flag if the values differ. The dirty flag
        is the flag which will require the data to be updated in the database
        as we know that the value is changed and needs to be copied out to
        the database.

        @param mixed value The new value to assign to the property

        """
        if value != self.__value:
            self.__dirty = True
            self.__value = value

    def is_dirty(self):
        """@return bool Whether or not this property is dirty"""
        return self.__dirty

    def set_dirty(self, dirty):
        """Reassigns the dirty value

        @param bool dirty Whether or not this cache object should be considered
            'dirty'

        """
        self.__dirty = dirty


class DataCache(BaseClass, dict):
    """Class to cache data retrieved from the database"""
    def __init__(self, *args, **kwargs):
        """Constructor, ensure all data caches have an ID cache"""
        super(DataCache, self).__init__(*args, **kwargs)
        self.id = None

    def __setitem__(self, key, value):
        """Overriden from dict base, ensure types are Cached Properties

        @param str key The name of the key-value pair
        @param mixed value The value to assign to the key

        """
        if key in self:
            property_ = super(DataCache, self).__getitem__(key)
            if isinstance(property_, CacheProperty):
                # Update the cached property value
                property_.value = value
                return
        if isinstance(value, dict):
            # All dicts should in turn be further data caches
            value = DataCache(value)
        elif not isinstance(value, CacheProperty):
            value = CacheProperty(value)
        super(DataCache, self).__setitem__(key, value)

    def __getitem__(self, key):
        """Overriden from base to obtain the actual value of cached properties

        Rather than return the CacheProperty object, we will return the value
        that the object represents in its natural data type. This is to make
        the dictionary feel more like a natural key/value pair rather than this
        CacheProperty hook.

        @param str key The name of the item to obtain the value for
        @return mixed The real value of the cached property

        """
        cached_property = super(DataCache, self).__getitem__(key)
        if isinstance(cached_property, CacheProperty):
            return cached_property.value
        return cached_property

    def get_dirty(self):
        """Obtains a shallow copy of all the dirty cached properties

        Recursively searches all DataCaches and returns a shallow copy of the
        instances. This will NOT copy the cache property objects, so do not
        manually delete them, let the garbage collector handle them.

        @return dict Shallow copy of the dirty cached values

        """
        dirty = DataCache()
        for key in self.keys():
            property_ = super(DataCache, self).__getitem__(key)
            if isinstance(property_, CacheProperty):
                if property_.is_dirty():
                    dirty[key] = property_
            elif isinstance(property_, DataCache):
                # Recursively obtain the dirty values in this data cache
                dirty[key] = property_.get_dirty()
            else:
                raise TypeError("Cannot obtain the dirty value of type: %s" %
                    type(property_).__name__)
        dirty.id = self.id
        return dirty

    def is_dirty(self, include_children=False):
        """Whether or not this object (or potentially children) are dirty

        A dirty object determines whether or not data in the cache needs
        to write back to the database; i.e local changes have occurred in the
        cache and needs to be reflected in the database.

        @param bool include_children Whether to include children in the dirty
            search
        @return bool Whether this object or any children are dirty

        """
        for property_ in self.values():
            if (isinstance(property_, CacheProperty) or
                include_children is True):
                if property_.is_dirty():
                    return True
        return False

    def is_new(self, include_children=False):
        """Whether or not this object (or potentially children is new)

        A new object determines whether it has been added to the cache since
        the last cache commit; i.e. the player exists soley in the cache and
        not in the database.

        @param bool include_children Whether to include children in the new
        @return bool Whether this object or any children are new

        """
        if self.id is None:
            return True
        if include_children is False:
            return False
        for property_ in self.values():
            # Loop through all child data caches
            if isinstance(property_, DataCache):
                if property_.id is None:
                    return True
        return False

    def set_dirty(self, dirty_flag):
        """Recursively assigns the dirty flag to all cache properties

        Loops through all data caches and cached properties to ensure that
        the dirty flag is set to the value we assign.

        @param bool dirty_flag What to assign the dirty flag to

        """
        for property_ in self.values():
            # Both the DataCache and CacheProperty have the set_dirty method
            property_.set_dirty(dirty_flag)
